﻿using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Reflection;

namespace DynamicMocker
{
	public class ObjectWrapper : DynamicObject
	{
		private readonly Type _originalType;
		private object _originalObject;
		private readonly object[] _constructorArguments;
		private Type[] _constructorArgumentTypes;

		public ObjectWrapper(Type originalType, params object[] constructorArguments)
		{
			_originalType = originalType;
			_constructorArguments = constructorArguments;
			MethodStubs = new Dictionary<string, List<MethodWrapper>>();
		}

		private Dictionary<string, List<MethodWrapper>> MethodStubs { get; set; }

		public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
		{
			var wrapper = GetFakeMethod(binder.Name);
			MethodInfo method;
			var obj = _originalObject;
			if (wrapper != null)
			{
				result = wrapper.Execute(args);
				return true;
			}

			if (_originalObject == null)
			{
				CreateOriginalInstance();
				obj = _originalObject;
			}
			var methodInfo = from mInfo in _originalType.GetMethods()
			                 where mInfo.Name == binder.Name
			                 select mInfo;

			if (methodInfo.Any())
			{
				method = methodInfo.First();
				result = method.Invoke(obj, args);
				return true;
			}
			return base.TryInvokeMember(binder, args, out result);
		}

		private MethodWrapper GetFakeMethod(string name)
		{
			if (MethodStubs.ContainsKey(name))
			{
				var methodWrappers = MethodStubs[name];
				if (methodWrappers.Count() > 0)
				{
					var mInfo = methodWrappers[0];
					return mInfo;
				}
			}
			return null;
		}

		private void CreateOriginalInstance()
		{
			var constructorInfo = _originalType.GetConstructor(GetConstructorArgumentTypes());
			_originalObject = constructorInfo.Invoke(_constructorArguments);
		}

		private Type[] GetConstructorArgumentTypes()
		{
			_constructorArgumentTypes = new Type[_constructorArguments.Length];
			for (int i = 0; i < _constructorArguments.Length; i++)
			{
				_constructorArgumentTypes[i] = _constructorArguments[i].GetType();
			}
			return _constructorArgumentTypes;
		}

		public bool AddFakeMethod(string methodName, MethodWrapper methodWrapper)
		{
			if (!MethodStubs.ContainsKey(methodName))
			{
				MethodStubs.Add(methodName, new List<MethodWrapper>());
			}
			MethodStubs[methodName].Add(methodWrapper);
			return true;
		}

		public bool Verify()
		{
			foreach (var methodStub in MethodStubs)
			{
				var wrappers = methodStub.Value;
				foreach (var methodWrapper in wrappers.Where(methodWrapper => !methodWrapper.Verify()))
				{
					throw new Exception(string.Format("Method {0} was called {1} times instead of {2}", methodStub.Key, methodWrapper.CallCount, methodWrapper.ExpectedCallCount));
				}
			}
			return true;
		}
	}
}